﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NDBGen.Descriptors;
using NDBGen.Descriptors.Tables.Tokens;

namespace NDBGen
{
    public class Table
    {
        internal Table()
            : this(null, null) { }

        internal Table(String input, String schema)
        {
            this.Input = input;
            this.Schema = schema ?? "dbo";
            
            this.Columns = new List<Column>();
            this.DescriptorList = new List<ITableTokenDescriptor>();

            this.TableTokenDescriptorLibary = new ExportLibrary<ITableTokenDescriptor, ITokenDescriptorBehaviors>(PluginLibraries.Catalog);
        }

        private ExportLibrary<ITableTokenDescriptor, ITokenDescriptorBehaviors> TableTokenDescriptorLibary { get; set; }

        public String Input { get; private set; }
        public TableKey Key { get { return new TableKey(this.Schema, this.Name); } }
        public String Schema { get; set; }
        public String Name { get; set; }
        public List<Column> Columns { get; set; }
        public ScriptedTableData[] ScriptedData { get; set; }
        public String TypeName { get { return this.Name.Depluralize().ToPascalCase(); } }
        public Boolean CanBeRepresentedByEnum { get { return this.Columns.Count == 2; } }
        public Boolean WasReplacedByEnum { get; set; }

        internal List<ITableTokenDescriptor> DescriptorList { get; set; }
        public ITableTokenDescriptor[] Descriptors { get { return this.DescriptorList.ToArray(); } }

        internal Table Parse() { return this.Parse(null); }

        public Table Parse(IGrouping<String, ScriptedTableData>[] groupedScriptedData)
        {
            var scriptedData = (groupedScriptedData ?? new IGrouping<String, ScriptedTableData>[0]).SelectMany(g => g);
            var lines = this.Input.GetLines().Where(str => str.Trim().Length > 0).ToList();
            
            // The first line is the table name. Also need to count the indentation.
            var indent = this.Input.CountStartingTabs();
            var parts = lines[0].Trim().Split('|');
            this.Name = parts[0].Trim();

            //if (scriptedData.Count(l => l.TableSchema == this.Schema && l.TableName == this.Name) > 1) throw new InvalidDataException(String.Format("Multiple lookup data entries were specified for {0}.{1}", this.Schema, this.Name));

            this.ScriptedData = scriptedData.Where(l => l.TableSchemaName == this.Schema && l.TableName == this.Name).ToArray();
            if (this.Name.Contains('.'))
            {
                var nameParts = this.Name.Split('.');
                this.Name = nameParts[1];
                this.Schema = nameParts[0];
            }
            lines.RemoveAt(0);

            if (parts.Length > 1)
            {
                var props = parts[1];
                var tokens = props.ToCharArray();

                for (var index = 0; index < tokens.Length; index++)
                {
                    var token = tokens[index];

                    var descriptorExport = this.TableTokenDescriptorLibary.FindOneWithMetadata(t => t.Value.Token == token);

                    if (descriptorExport == null) continue;

                    var tokenHasValue = descriptorExport.Metadata.HasValue;

                    String tokenValue = token.ToString();
                    if (tokenHasValue)
                    {
                        var nextTokenIndex = GetIndexOfNextToken(index + 1, tokens);
                        if (nextTokenIndex > 0) tokenValue = props.Substring(index + 1, nextTokenIndex - index + 1);
                        else tokenValue = props.Substring(index + 1);
                    }

                    descriptorExport.Value.Init(this.Name);

                    this.DescriptorList.Add(descriptorExport.Value);
                }
            }

            // Subsequent lines are the columns. They must be indented 1 more tabs than the table was.
            foreach (var line in lines)
            {
                if (line.CountStartingTabs() != indent + 1) throw new FormatException("The columns in a table definition need to be indented 1 more tab that the table name.");
                else
                {
                    var column = new Column(this, line);
                    column.Parse();
                    this.Columns.Add(column);
                }
            }

            return this;
        }

        private Int32 GetIndexOfNextToken(Int32 start, Char[] tokens)
        {
            for (var index = start; index < tokens.Length; index++)
            {
                // get the first ITableTokenDescriptor implementation that matches the descriptor
                var handler = this.TableTokenDescriptorLibary.FindOneWithMetadata(t => t.Value.Token == tokens[index]);

                // if there is no descriptorExport, skip this descriptor
                if (handler == null) continue;
                else return index;
            }

            return -1;
        }

        internal Table AddDescriptor<T>(String tokenValue)
            where T : ITableTokenDescriptor, new()
        {
            var descriptor = new T();
            descriptor.Init(this.Name);
            this.DescriptorList.Add(descriptor);

            return this;
        }

        internal Table AddDescriptors(params ITableTokenDescriptor[] descriptors)
        {
            foreach (var descriptor in descriptors) descriptor.Init(this.Name);
            this.DescriptorList.AddRange(descriptors);

            return this;
        }

        public override Boolean Equals(Object obj)
        {
            var that = obj as Table;
            if (that == null) return false;
            if (this.Columns.Count != that.Columns.Count) return false;
            if (this.Descriptors.Count() != that.Descriptors.Count()) return false;

            // Compare each column
            for (Int32 i = 0; i < this.Columns.Count; i++) if (!this.Columns[i].Equals(that.Columns[i])) return false;

            // Compare descriptors
            for (Int32 i = 0; i < this.Descriptors.Length; i++) if (!this.Descriptors[i].Equals(that.Descriptors[i])) return false;

            return this.Name == that.Name && this.Schema == that.Schema;
        }

        public override String ToString()
        {
            var sb = new StringBuilder();
            sb.AppendFormat("{0}.{1}", this.Schema, this.Name);
            if (this.Descriptors.Any())
            {
                sb.Append('|');
                foreach (var descriptor in this.Descriptors) sb.Append(descriptor);
            }
            sb.AppendLine();
            foreach (var col in this.Columns)
            {
                sb.AppendLine();
                sb.AppendFormat("\t{0}", col);
            }
            return sb.ToString();
        }
    }
}